/**
 * $Revision$
 * $Date$
 *
 * Copyright 2006-2007 Jive Software.
 *
 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jivesoftware.smack;

import org.jivesoftware.smack.filter.*;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.Privacy;
import org.jivesoftware.smack.packet.PrivacyItem;

import java.util.*;

/**
 * A PrivacyListManager is used by XMPP clients to block or allow communications
 * from other users. Use the manager to:
 * <ul>
 * <li>Retrieve privacy lists.
 * <li>Add, remove, and edit privacy lists.
 * <li>Set, change, or decline active lists.
 * <li>Set, change, or decline the default list (i.e., the list that is active
 * by default).
 * </ul>
 * Privacy Items can handle different kind of permission communications based on
 * JID, group, subscription type or globally (@see PrivacyItem).
 * 
 * @author Francisco Vives
 */
public class PrivacyListManager {

	// Keep the list of instances of this class.
	private static Map<Connection, PrivacyListManager> instances = Collections
			.synchronizedMap(new WeakHashMap<Connection, PrivacyListManager>());

	private Connection connection;
	private final List<PrivacyListListener> listeners = new ArrayList<PrivacyListListener>();
	PacketFilter packetFilter = new AndFilter(new IQTypeFilter(IQ.Type.SET),
			new PacketExtensionFilter("query", "jabber:iq:privacy"));

	static {
		// Create a new PrivacyListManager on every established connection. In
		// the init()
		// method of PrivacyListManager, we'll add a listener that will delete
		// the
		// instance when the connection is closed.
		Connection
				.addConnectionCreationListener(new ConnectionCreationListener() {
					public void connectionCreated(Connection connection) {
						new PrivacyListManager(connection);
					}
				});
	}

	/**
	 * Creates a new privacy manager to maintain the communication privacy.
	 * Note: no information is sent to or received from the server until you
	 * attempt to get or set the privacy communication.
	 * <p>
	 * 
	 * @param connection
	 *            the XMPP connection.
	 */
	private PrivacyListManager(Connection connection) {
		this.connection = connection;
		this.init();
	}

	/**
	 * Answer the connection userJID that owns the privacy.
	 * 
	 * @return the userJID that owns the privacy
	 */
	private String getUser() {
		return connection.getUser();
	}

	/**
	 * Initializes the packet listeners of the connection that will notify for
	 * any set privacy package.
	 */
	private void init() {
		// Register the new instance and associate it with the connection
		instances.put(connection, this);
		// Add a listener to the connection that removes the registered instance
		// when
		// the connection is closed
		connection.addConnectionListener(new ConnectionListener() {
			public void connectionClosed() {
				// Unregister this instance since the connection has been closed
				instances.remove(connection);
			}

			public void connectionClosedOnError(Exception e) {
				// ignore
			}

			public void reconnectionFailed(Exception e) {
				// ignore
			}

			public void reconnectingIn(int seconds) {
				// ignore
			}

			public void reconnectionSuccessful() {
				// ignore
			}
		});

		connection.addPacketListener(new PacketListener() {
			public void processPacket(Packet packet) {

				if (packet == null || packet.getError() != null) {
					return;
				}
				// The packet is correct.
				Privacy privacy = (Privacy) packet;

				// Notifies the event to the listeners.
				synchronized (listeners) {
					for (PrivacyListListener listener : listeners) {
						// Notifies the created or updated privacy lists
						for (Map.Entry<String, List<PrivacyItem>> entry : privacy
								.getItemLists().entrySet()) {
							String listName = entry.getKey();
							List<PrivacyItem> items = entry.getValue();
							if (items.isEmpty()) {
								listener.updatedPrivacyList(listName);
							} else {
								listener.setPrivacyList(listName, items);
							}
						}
					}
				}

				// Send a result package acknowledging the reception of a
				// privacy package.

				// Prepare the IQ packet to send
				IQ iq = new IQ() {
					public String getChildElementXML() {
						return "";
					}
				};
				iq.setType(IQ.Type.RESULT);
				iq.setFrom(packet.getFrom());
				iq.setPacketID(packet.getPacketID());

				// Send create & join packet.
				connection.sendPacket(iq);
			}
		}, packetFilter);
	}

	/**
	 * Returns the PrivacyListManager instance associated with a given
	 * Connection.
	 * 
	 * @param connection
	 *            the connection used to look for the proper PrivacyListManager.
	 * @return the PrivacyListManager associated with a given Connection.
	 */
	public static PrivacyListManager getInstanceFor(Connection connection) {
		return instances.get(connection);
	}

	/**
	 * Send the {@link Privacy} packet to the server in order to know some
	 * privacy content and then waits for the answer.
	 * 
	 * @param requestPrivacy
	 *            is the {@link Privacy} packet configured properly whose XML
	 *            will be sent to the server.
	 * @return a new {@link Privacy} with the data received from the server.
	 * @exception XMPPException
	 *                if the request or the answer failed, it raises an
	 *                exception.
	 */
	private Privacy getRequest(Privacy requestPrivacy) throws XMPPException {
		// The request is a get iq type
		requestPrivacy.setType(Privacy.Type.GET);
		requestPrivacy.setFrom(this.getUser());

		// Filter packets looking for an answer from the server.
		PacketFilter responseFilter = new PacketIDFilter(
				requestPrivacy.getPacketID());
		PacketCollector response = connection
				.createPacketCollector(responseFilter);

		// Send create & join packet.
		connection.sendPacket(requestPrivacy);

		// Wait up to a certain number of seconds for a reply.
		Privacy privacyAnswer = (Privacy) response
				.nextResult(SmackConfiguration.getPacketReplyTimeout());

		// Stop queuing results
		response.cancel();

		// Interprete the result and answer the privacy only if it is valid
		if (privacyAnswer == null) {
			throw new XMPPException("No response from server.");
		} else if (privacyAnswer.getError() != null) {
			throw new XMPPException(privacyAnswer.getError());
		}
		return privacyAnswer;
	}

	/**
	 * Send the {@link Privacy} packet to the server in order to modify the
	 * server privacy and waits for the answer.
	 * 
	 * @param requestPrivacy
	 *            is the {@link Privacy} packet configured properly whose xml
	 *            will be sent to the server.
	 * @return a new {@link Privacy} with the data received from the server.
	 * @exception XMPPException
	 *                if the request or the answer failed, it raises an
	 *                exception.
	 */
	private Packet setRequest(Privacy requestPrivacy) throws XMPPException {

		// The request is a get iq type
		requestPrivacy.setType(Privacy.Type.SET);
		requestPrivacy.setFrom(this.getUser());

		// Filter packets looking for an answer from the server.
		PacketFilter responseFilter = new PacketIDFilter(
				requestPrivacy.getPacketID());
		PacketCollector response = connection
				.createPacketCollector(responseFilter);

		// Send create & join packet.
		connection.sendPacket(requestPrivacy);

		// Wait up to a certain number of seconds for a reply.
		Packet privacyAnswer = response.nextResult(SmackConfiguration
				.getPacketReplyTimeout());

		// Stop queuing results
		response.cancel();

		// Interprete the result and answer the privacy only if it is valid
		if (privacyAnswer == null) {
			throw new XMPPException("No response from server.");
		} else if (privacyAnswer.getError() != null) {
			throw new XMPPException(privacyAnswer.getError());
		}
		return privacyAnswer;
	}

	/**
	 * Answer a privacy containing the list structre without {@link PrivacyItem}
	 * .
	 * 
	 * @return a Privacy with the list names.
	 * @throws XMPPException
	 *             if an error occurs.
	 */
	private Privacy getPrivacyWithListNames() throws XMPPException {

		// The request of the list is an empty privacy message
		Privacy request = new Privacy();

		// Send the package to the server and get the answer
		return getRequest(request);
	}

	/**
	 * Answer the active privacy list.
	 * 
	 * @return the privacy list of the active list.
	 * @throws XMPPException
	 *             if an error occurs.
	 */
	public PrivacyList getActiveList() throws XMPPException {
		Privacy privacyAnswer = this.getPrivacyWithListNames();
		String listName = privacyAnswer.getActiveName();
		boolean isDefaultAndActive = privacyAnswer.getActiveName() != null
				&& privacyAnswer.getDefaultName() != null
				&& privacyAnswer.getActiveName().equals(
						privacyAnswer.getDefaultName());
		return new PrivacyList(true, isDefaultAndActive, listName,
				getPrivacyListItems(listName));
	}

	/**
	 * Answer the default privacy list.
	 * 
	 * @return the privacy list of the default list.
	 * @throws XMPPException
	 *             if an error occurs.
	 */
	public PrivacyList getDefaultList() throws XMPPException {
		Privacy privacyAnswer = this.getPrivacyWithListNames();
		String listName = privacyAnswer.getDefaultName();
		boolean isDefaultAndActive = privacyAnswer.getActiveName() != null
				&& privacyAnswer.getDefaultName() != null
				&& privacyAnswer.getActiveName().equals(
						privacyAnswer.getDefaultName());
		return new PrivacyList(isDefaultAndActive, true, listName,
				getPrivacyListItems(listName));
	}

	/**
	 * Answer the privacy list items under listName with the allowed and blocked
	 * permissions.
	 * 
	 * @param listName
	 *            the name of the list to get the allowed and blocked
	 *            permissions.
	 * @return a list of privacy items under the list listName.
	 * @throws XMPPException
	 *             if an error occurs.
	 */
	private List<PrivacyItem> getPrivacyListItems(String listName)
			throws XMPPException {

		// The request of the list is an privacy message with an empty list
		Privacy request = new Privacy();
		request.setPrivacyList(listName, new ArrayList<PrivacyItem>());

		// Send the package to the server and get the answer
		Privacy privacyAnswer = getRequest(request);

		return privacyAnswer.getPrivacyList(listName);
	}

	/**
	 * Answer the privacy list items under listName with the allowed and blocked
	 * permissions.
	 * 
	 * @param listName
	 *            the name of the list to get the allowed and blocked
	 *            permissions.
	 * @return a privacy list under the list listName.
	 * @throws XMPPException
	 *             if an error occurs.
	 */
	public PrivacyList getPrivacyList(String listName) throws XMPPException {

		return new PrivacyList(false, false, listName,
				getPrivacyListItems(listName));
	}

	/**
	 * Answer every privacy list with the allowed and blocked permissions.
	 * 
	 * @return an array of privacy lists.
	 * @throws XMPPException
	 *             if an error occurs.
	 */
	public PrivacyList[] getPrivacyLists() throws XMPPException {
		Privacy privacyAnswer = this.getPrivacyWithListNames();
		Set<String> names = privacyAnswer.getPrivacyListNames();
		PrivacyList[] lists = new PrivacyList[names.size()];
		boolean isActiveList;
		boolean isDefaultList;
		int index = 0;
		for (String listName : names) {
			isActiveList = listName.equals(privacyAnswer.getActiveName());
			isDefaultList = listName.equals(privacyAnswer.getDefaultName());
			lists[index] = new PrivacyList(isActiveList, isDefaultList,
					listName, getPrivacyListItems(listName));
			index = index + 1;
		}
		return lists;
	}

	/**
	 * Set or change the active list to listName.
	 * 
	 * @param listName
	 *            the list name to set as the active one.
	 * @exception XMPPException
	 *                if the request or the answer failed, it raises an
	 *                exception.
	 */
	public void setActiveListName(String listName) throws XMPPException {

		// The request of the list is an privacy message with an empty list
		Privacy request = new Privacy();
		request.setActiveName(listName);

		// Send the package to the server
		setRequest(request);
	}

	/**
	 * Client declines the use of active lists.
	 * 
	 * @throws XMPPException
	 *             if an error occurs.
	 */
	public void declineActiveList() throws XMPPException {

		// The request of the list is an privacy message with an empty list
		Privacy request = new Privacy();
		request.setDeclineActiveList(true);

		// Send the package to the server
		setRequest(request);
	}

	/**
	 * Set or change the default list to listName.
	 * 
	 * @param listName
	 *            the list name to set as the default one.
	 * @exception XMPPException
	 *                if the request or the answer failed, it raises an
	 *                exception.
	 */
	public void setDefaultListName(String listName) throws XMPPException {

		// The request of the list is an privacy message with an empty list
		Privacy request = new Privacy();
		request.setDefaultName(listName);

		// Send the package to the server
		setRequest(request);
	}

	/**
	 * Client declines the use of default lists.
	 * 
	 * @throws XMPPException
	 *             if an error occurs.
	 */
	public void declineDefaultList() throws XMPPException {

		// The request of the list is an privacy message with an empty list
		Privacy request = new Privacy();
		request.setDeclineDefaultList(true);

		// Send the package to the server
		setRequest(request);
	}

	/**
	 * The client has created a new list. It send the new one to the server.
	 * 
	 * @param listName
	 *            the list that has changed its content.
	 * @param privacyItems
	 *            a List with every privacy item in the list.
	 * @throws XMPPException
	 *             if an error occurs.
	 */
	public void createPrivacyList(String listName,
			List<PrivacyItem> privacyItems) throws XMPPException {

		this.updatePrivacyList(listName, privacyItems);
	}

	/**
	 * The client has edited an existing list. It updates the server content
	 * with the resulting list of privacy items. The {@link PrivacyItem} list
	 * MUST contain all elements in the list (not the "delta").
	 * 
	 * @param listName
	 *            the list that has changed its content.
	 * @param privacyItems
	 *            a List with every privacy item in the list.
	 * @throws XMPPException
	 *             if an error occurs.
	 */
	public void updatePrivacyList(String listName,
			List<PrivacyItem> privacyItems) throws XMPPException {

		// Build the privacy package to add or update the new list
		Privacy request = new Privacy();
		request.setPrivacyList(listName, privacyItems);

		// Send the package to the server
		setRequest(request);
	}

	/**
	 * Remove a privacy list.
	 * 
	 * @param listName
	 *            the list that has changed its content.
	 * @throws XMPPException
	 *             if an error occurs.
	 */
	public void deletePrivacyList(String listName) throws XMPPException {

		// The request of the list is an privacy message with an empty list
		Privacy request = new Privacy();
		request.setPrivacyList(listName, new ArrayList<PrivacyItem>());

		// Send the package to the server
		setRequest(request);
	}

	/**
	 * Adds a packet listener that will be notified of any new update in the
	 * user privacy communication.
	 * 
	 * @param listener
	 *            a packet listener.
	 */
	public void addListener(PrivacyListListener listener) {
		// Keep track of the listener so that we can manually deliver extra
		// messages to it later if needed.
		synchronized (listeners) {
			listeners.add(listener);
		}
	}
}
